﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Xml;

namespace PartUpdater
{
    class PartUpdater
    {
        private readonly string _pathToServer;
        private readonly string _pathToClient;

        private readonly DirectoryInfo _serverInfo;
        private readonly DirectoryInfo _clientInfo;

        private string[] _serverFiles;
        private readonly string[] _clientFiles;

        private string[] _serverCrc;
        private readonly string[] _clientCrc;

        private readonly Hashtable _server;
        private readonly Hashtable _client;

        public string[] MustCopy; // Файлы, нуждающиеся в копировании

        private readonly double _mustCopySize; // Размер всех копируемых файлов

        private double _copiedSize; // Размер скопированных данных

        private bool _canContinued; // Может ли продолжиться копирование недокопированного файла

        public PartUpdater(string pathFrom, string pathTo)
        {
            _pathToServer = pathFrom;
            _pathToClient = pathTo;

            _server = new Hashtable();
            _client = new Hashtable();

            _serverInfo = new DirectoryInfo(pathFrom);
            _clientInfo = new DirectoryInfo(pathTo);

            LoadInfoFile(Path.Combine(_pathToServer, "info.xml"));

            _clientFiles = Directory.GetFiles(pathTo, "*.*", SearchOption.AllDirectories);

            _clientCrc = GetCrc(_clientFiles);

            _clientFiles = GetRelativePathes(_clientFiles, _clientInfo);

            SetTableInfo();

            MustCopy = GetMustCopyFiles();

            _mustCopySize = CalcFilesSize(true);
            _copiedSize = CalcFilesSize(false);
        }

        public bool CopyFiles()
        {
            if (MustCopy.Length == 0) return false;

            if (_mustCopySize > GetFreeSpace()) throw new Exception("Недостаточно свобоного места на диске");

            CreateNotFoundedCatalogs();

            foreach (var clientFile in _clientFiles)
            {
                _server.Remove(clientFile);
                if (Path.GetExtension(Path.Combine(_pathToClient, clientFile)) == ".tmpfile")
                {
                    FastCopy(Path.Combine(_pathToServer, clientFile), Path.Combine(_pathToClient, clientFile));
                    List<string> tmp = MustCopy.ToList<string>();
                    tmp.Remove(clientFile.Substring(0, clientFile.Length-8));
                    MustCopy = tmp.ToArray();
                }
            }

            foreach (var f in MustCopy)
            {
                try
                {
                    ASyncCopy(Path.Combine(_pathToServer, f), Path.Combine(_pathToClient, f));
                }
                catch (Exception e)
                {
                    Console.Error.WriteLine(e);
                }
            }
            return true;
        }

        private long GetFreeSpace()
        {
            var di = new DriveInfo(this._pathToClient.Substring(0, 2));
            return di.TotalFreeSpace;
        }

        /// <summary>
        /// Метод быстрого копирования
        /// </summary>
        /// <param name="from">Путь к каталогу на сервере</param>
        /// <param name="to">Путь к каталогу на клиенте</param>
        public void FastCopy(string from, string to) {
            const int copyBufferSize = 64 * 1024;

            using (var outputFile = File.OpenWrite(to.Substring(0, from.Length - 7)))
            {
                using (var inputFile = File.OpenRead(from.Substring(0, from.Length - 7)))
                {
                    var size = outputFile.Length;
                    var buffer = new byte[copyBufferSize];
                    int bytesRead;
                    inputFile.Position = size;
                    outputFile.Position = size;
                    while ((bytesRead = inputFile.Read(buffer, 0, copyBufferSize)) != 0) {
                        outputFile.Write(buffer, 0, bytesRead);
                        _copiedSize += bytesRead;
                        ChangeProgress(_copiedSize);
                    }
                }
            }
            File.Delete(to);
        }

        public void ASyncCopy(string from, string to) {
            const int copyBufferSize = 64 * 1024;

            if (File.Exists(to))
            {
                try
                {
                    File.Delete(to);
                }
                catch (Exception e)
                {
                    Console.Error.Write(e.Message);
                }
            }
                Copy(from, to, copyBufferSize);
        }

        private void Copy(string from, string to, int copyBufferSize)
        {
            var tempFile = File.Create(to + ".tmpfile");
            using (var outputFile = File.Create(to))
            {
                using (var inputFile = File.OpenRead(from))
                {
                    var buffer1 = new byte[copyBufferSize];
                    var buffer2 = new byte[copyBufferSize];
                    var inputBuffer = buffer1;
                    int bytesRead;
                    IAsyncResult writeResult = null;
                    while ((bytesRead = inputFile.Read(inputBuffer, 0, copyBufferSize)) != 0)
                    {
                        if (writeResult != null)
                        {
                            writeResult.AsyncWaitHandle.WaitOne();
                            outputFile.EndWrite(writeResult);
                            writeResult = null;
                        }
                        var outputBuffer = inputBuffer;
                        inputBuffer = (inputBuffer == buffer1) ? buffer2 : buffer1;
                        writeResult = outputFile.BeginWrite(outputBuffer, 0, bytesRead, null, null);
                        _copiedSize += bytesRead;
                        ChangeProgress(_copiedSize);
                    }
                    if (writeResult != null)
                    {
                        writeResult.AsyncWaitHandle.WaitOne();
                        outputFile.EndWrite(writeResult);
                    }
                }
            }
            tempFile.Close();
            File.Delete(to + ".tmpfile");
        }

        private void ChangeProgress(double size)
        {
            Console.Write("\r{0}", arg0: (int) Math.Round(((size/_mustCopySize)*100)) + "%");
        }

        private double CalcFilesSize(bool server)
        {
            string[] sFiles;
            if (server)
            {
                sFiles = Directory.GetFiles(_pathToServer, "*.*", SearchOption.AllDirectories);
            }
            else {
                sFiles = Directory.GetFiles(_pathToClient, "*.*", SearchOption.AllDirectories);
            }

            return sFiles.Select(f => new FileInfo(f)).Aggregate(0.0, (current, fi) => current + fi.Length);
        }

        private void CreateNotFoundedCatalogs()
        {
            var serverList = new List<string>();
            var clientList = new List<string>();

            serverList.AddRange(GetRelativePathes(Directory.GetDirectories(_pathToServer, ".", SearchOption.AllDirectories), _serverInfo));
            clientList.AddRange(GetRelativePathes(Directory.GetDirectories(_pathToClient, ".", SearchOption.AllDirectories), _clientInfo));

            foreach (var dir in serverList.Where(dir => !Directory.Exists(Path.Combine(_pathToClient, dir))))
            {
                Directory.CreateDirectory(Path.Combine(_pathToClient, dir));
            }

        }

        private void LoadInfoFile(string path) {
            if (File.Exists(Path.Combine(this._pathToClient, "info.xml")))
            {
                if (GetMd5HashFromFile(path) != GetMd5HashFromFile(Path.Combine(this._pathToClient, "info.xml")))
                {
                    File.Copy(path, Path.Combine(this._pathToClient, "info.xml"), true);
                }
                else
                {
                    _canContinued = true;
                }
            }
            else 
            {
                File.Copy(path, Path.Combine(this._pathToClient, "info.xml"), true);
            }

            var files = new List<string>();
            var crc = new List<string>();

            var xdoc = new XmlDocument();
            xdoc.Load(path);
            try
            {
                var filesInfo = xdoc.GetElementsByTagName("File");
                foreach (XmlNode f in filesInfo)
                {
                    if (f.Attributes != null)
                    {
                        files.Add(f.Attributes["path"].Value);
                        crc.Add(f.Attributes["hash"].Value);
                    }
                }
            }
            catch (Exception e) {
                Console.Write(e);
            }
            this._serverFiles = files.ToArray();
            this._serverCrc = crc.ToArray();
        }

        private string[] GetMustCopyFiles()
        {
            var list = new List<string>();
            for (var i = 0; i < _server.Count; i++)
            {
                if (_client.ContainsKey(_serverFiles[i]))
                {
                    if (!_client[_serverFiles[i]].Equals(_server[_serverFiles[i]]))
                    {
                        list.Add(_serverFiles[i]);
                    }
                }
                else
                {
                    list.Add(_serverFiles[i]);
                }
            }
            return list.ToArray();
        }

        private void SetTableInfo()
        {
            for (var i = 0; i < _serverFiles.Length; i++)
            {
                _server.Add(_serverFiles[i], _serverCrc[i]);
            }
            for (var i = 0; i < _clientFiles.Length; i++)
            {
                _client.Add(_clientFiles[i], _clientCrc[i]);
            }
        }

        private string[] GetCrc(IEnumerable<string> files)
        {
            return files.Select(GetMd5HashFromFile).ToArray();
        }

        private string[] GetRelativePathes(IEnumerable<string> files, DirectoryInfo dir)
        {
            return files.Select(t => new FileInfo(t)).Select(info => info.FullName.Substring(dir.FullName.IndexOf(dir.Name, System.StringComparison.Ordinal) + dir.Name.Count() + 1)).ToArray();
        }

        private string GetMd5HashFromFile(string fileName)
        {
            using (var file = new FileStream(fileName, FileMode.Open))
            {
                byte[] retVal = CalculateHash(file);
                return GetStringHash(retVal);
            }
        }

        private string GetStringHash(byte[] retVal)
        {
            var stringBuilder = new StringBuilder();

            for (int i = 0; i < retVal.Length; i++)
            {
                stringBuilder.Append(retVal[i].ToString("x2"));
            }

            return stringBuilder.ToString();
        }

        private byte[] CalculateHash(Stream file)
        {
            using (MD5 md5 = new MD5CryptoServiceProvider())
            {
                return md5.ComputeHash(file);
            }
        }

    }
}
